package jadx.core.utils.android;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jadx.api.ICodeWriter;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.api.plugins.input.data.annotations.EncodedType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.core.codegen.ClassGen;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.ConstStorage;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.xmlgen.ResourceStorage;
import jadx.core.xmlgen.entry.ResourceEntry;

/**
 * Android resources specific handlers
 */
public class AndroidResourcesUtils {
	private static final Logger LOG = LoggerFactory.getLogger(AndroidResourcesUtils.class);

	private AndroidResourcesUtils() {
	}

	public static ClassNode searchAppResClass(RootNode root, ResourceStorage resStorage) {
		String appPackage = root.getAppPackage();
		String fullName = appPackage != null ? appPackage + ".R" : "R";
		ClassInfo clsInfo = ClassInfo.fromName(root, fullName);
		ClassNode resCls = root.resolveClass(clsInfo);
		if (resCls != null) {
			addResourceFields(resCls, resStorage, true);
			return resCls;
		}
		LOG.info("Can't find 'R' class in app package: {}", appPackage);
		List<ClassNode> candidates = root.searchClassByShortName("R");
		if (candidates.size() == 1) {
			ClassNode resClsCandidate = candidates.get(0);
			addResourceFields(resClsCandidate, resStorage, true);
			return resClsCandidate;
		}
		if (!candidates.isEmpty()) {
			LOG.info("Found several 'R' class candidates: {}", candidates);
		}
		LOG.info("App 'R' class not found, put all resources ids into : '{}'", fullName);
		ClassNode rCls = ClassNode.addSyntheticClass(root, clsInfo, AccessFlags.PUBLIC | AccessFlags.FINAL);
		rCls.addInfoComment("This class is generated by JADX");
		addResourceFields(rCls, resStorage, false);
		return rCls;
	}

	public static boolean handleAppResField(ICodeWriter code, ClassGen clsGen, ClassInfo declClass) {
		ClassInfo parentClass = declClass.getParentClass();
		if (parentClass != null && parentClass.getShortName().equals("R")) {
			clsGen.useClass(code, parentClass);
			code.add('.');
			code.add(declClass.getAliasShortName());
			return true;
		}
		return false;
	}

	/**
	 * Force hex format for Android resources ids
	 */
	public static boolean isResourceFieldValue(ClassNode cls, ArgType type) {
		return type.equals(ArgType.INT) && isResourceClass(cls);
	}

	public static boolean isResourceClass(ClassNode cls) {
		ClassNode parentClass = cls.getParentClass();
		return parentClass != null && parentClass.getAlias().equals("R");
	}

	private static final class ResClsInfo {
		private final ClassNode typeCls;
		private final Map<String, FieldNode> fieldsMap = new HashMap<>();

		private ResClsInfo(ClassNode typeCls) {
			this.typeCls = typeCls;
		}

		public ClassNode getTypeCls() {
			return typeCls;
		}

		public Map<String, FieldNode> getFieldsMap() {
			return fieldsMap;
		}
	}

	private static void addResourceFields(ClassNode resCls, ResourceStorage resStorage, boolean rClsExists) {
		Map<Integer, FieldNode> resFieldsMap = fillResFieldsMap(resCls);
		Map<String, ResClsInfo> innerClsMap = new TreeMap<>();
		if (rClsExists) {
			for (ClassNode innerClass : resCls.getInnerClasses()) {
				ResClsInfo innerResCls = new ResClsInfo(innerClass);
				innerClass.getFields().forEach(field -> innerResCls.getFieldsMap().put(field.getName(), field));
				innerClsMap.put(innerClass.getAlias(), innerResCls);
			}
		}
		for (ResourceEntry resource : resStorage.getResources()) {
			String resTypeName = resource.getTypeName();
			String resName = resource.getKeyName().replace('.', '_');

			ResClsInfo typeClsInfo = innerClsMap.computeIfAbsent(
					resTypeName,
					name -> getClassForResType(resCls, rClsExists, name));
			typeClsInfo.getFieldsMap().computeIfAbsent(resName, name -> {
				ClassNode typeCls = typeClsInfo.getTypeCls();
				FieldInfo rFieldInfo = FieldInfo.from(typeCls.root(), typeCls.getClassInfo(), resName, ArgType.INT);
				FieldNode newResField = new FieldNode(typeCls, rFieldInfo,
						AccessFlags.PUBLIC | AccessFlags.STATIC | AccessFlags.FINAL);
				newResField.addAttr(new EncodedValue(EncodedType.ENCODED_INT, resource.getId()));
				typeCls.addField(newResField);
				if (rClsExists) {
					newResField.addInfoComment("Added by JADX");
				}
				return newResField;
			});
			FieldNode fieldNode = resFieldsMap.get(resource.getId());
			if (fieldNode != null
					&& !fieldNode.getName().equals(resName)
					&& NameMapper.isValidAndPrintable(resName)
					&& resCls.root().getArgs().isRenameValid()) {
				fieldNode.add(AFlag.DONT_RENAME);
				fieldNode.getFieldInfo().setAlias(resName);
			}
		}
	}

	private static ResClsInfo getClassForResType(ClassNode resCls, boolean rClsExists, String typeName) {
		RootNode root = resCls.root();
		String typeClsFullName = resCls.getClassInfo().makeRawFullName() + '$' + typeName;
		ClassInfo clsInfo = ClassInfo.fromName(root, typeClsFullName);
		ClassNode existCls = root.resolveClass(clsInfo);
		if (existCls != null) {
			ResClsInfo resClsInfo = new ResClsInfo(existCls);
			existCls.getFields().forEach(field -> resClsInfo.getFieldsMap().put(field.getName(), field));
			return resClsInfo;
		}
		ClassNode newTypeCls = ClassNode.addSyntheticClass(root, clsInfo,
				AccessFlags.PUBLIC | AccessFlags.STATIC | AccessFlags.FINAL);
		if (rClsExists) {
			newTypeCls.addInfoComment("Added by JADX");
		}
		return new ResClsInfo(newTypeCls);
	}

	@NotNull
	private static Map<Integer, FieldNode> fillResFieldsMap(ClassNode resCls) {
		Map<Integer, FieldNode> resFieldsMap = new HashMap<>();
		ConstStorage constStorage = resCls.root().getConstValues();
		constStorage.getGlobalConstFields().forEach((key, field) -> {
			if (field.getFieldInfo().getType().equals(ArgType.INT)
					&& field instanceof FieldNode
					&& key instanceof Integer) {
				FieldNode fldNode = (FieldNode) field;
				AccessInfo accessFlags = fldNode.getAccessFlags();
				if (accessFlags.isStatic() && accessFlags.isFinal()) {
					resFieldsMap.put((Integer) key, fldNode);
				}
			}
		});
		return resFieldsMap;
	}
}
